/*
 * Copyright (c) 2007 The Trustees of Indiana University. All rights reserved.
 * IU CSCI B534 Distributed Systems 2007-02-01 http://www.cs.indiana.edu/classes/b534
 */

package edu.indiana.cs.b534.torrent.impl;

import edu.indiana.cs.b534.torrent.InfoDictionary;
import edu.indiana.cs.b534.torrent.StorageManager;
import edu.indiana.cs.b534.torrent.TorrentException;
import edu.indiana.cs.b534.torrent.TorrentMetainfo;
import edu.indiana.cs.b534.torrent.Utils;
import edu.indiana.cs.b534.torrent.context.PeerDescription;
import edu.indiana.cs.b534.torrent.context.TorrentContext;
import edu.indiana.cs.b534.torrent.impl.message.TrackerRequestImpl;
import edu.indiana.cs.b534.torrent.impl.message.TrackerResponseImpl;
import edu.indiana.cs.b534.torrent.message.PeerDictionary;
import edu.indiana.cs.b534.torrent.message.TrackerRequest;
import edu.indiana.cs.b534.torrent.message.TrackerResponse;
import edu.indiana.cs.b534.torrent.struct.PiecePeerMapping;
import edu.indiana.cs.b534.torrent.struct.TorrentMetainfoImpl;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Vector;
import java.util.logging.Logger;

/**
 * Manages the download of a single torrentMetaInfo and associated pieces
 */
public class TorrentManager implements PieceManager, Runnable {

    //    private static Logger LOGGER;
    //
    //    public static Logger getLogger(){
    //    	return LOGGER.getLogger(TORRENT_MANAGER_NAME);
    //    }
    public static final String TORRENT_MANAGER_NAME = "edu.indiana.cs.b534.torrentMetaInfo.impl.TorrentManager";
    private static Logger log = Logger.getLogger(TORRENT_MANAGER_NAME);
    private TorrentContext torrentContext;
    // Shared data structures
    private TorrentMetainfo torrentMetaInfo;

    private String myIpAddress;
    private int myPort;

    private List<PieceInfo> piecesToBeDownloaded = new Vector<PieceInfo>();
    private List<PieceInfo> piecesBeingDownloaded = new Vector<PieceInfo>();
    private Map<Integer, PieceInfo> piecesFinishedDownloading = new HashMap<Integer, PieceInfo>();

    private byte[] infoHash;
    private BittorrentServer bittorrentServer;
    private HashMap<String, Boolean> peersJustGotBusy;
    private StorageManager storageManager;

    // data structure for maintaining peer statistics. How do you update or use this?
    // private Map<String, PeerStats> peerStatistics;

    public TorrentManager(TorrentMetainfo torrent, TorrentContext torrentContext) throws TorrentException {
        try {
            this.torrentMetaInfo = torrent;
            this.torrentContext = torrentContext;

            init();
            log.info("Torrent Manager Initalized");
        } catch (UnknownHostException e) {
            throw new TorrentException(e);
        } catch (SocketException e) {
            throw new TorrentException(e);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    private void init() throws TorrentException, UnknownHostException, SocketException, UnsupportedEncodingException {
        // we need to compute a hash for the file as it should be included whilst communicating
        // with the tracker.
        initInfoHash();

        myIpAddress = Utils.getIpAddress();

        torrentContext.setProperty(TorrentContext.TORRENT_META_INFO, torrentMetaInfo);
        myPort = torrentContext.getPort();
//        try {
//            FileHandler handler = new FileHandler("torrentMetaInfo-client.log", true);
//
//            // Add to the desired logger
//            LOGGER = Logger.getLogger(TORRENT_MANAGER_NAME);
//            LOGGER.addHandler(handler);
//        } catch (IOException e) {
//            e.printStackTrace();
//            System.out.println("Exception initializing the logger. ");
//        }
        char[] piecesInTargetFile = torrentMetaInfo.getInfo().getPieces().getValue().toCharArray();
        assert piecesInTargetFile.length % 20 == 0;
        int piecesCount = piecesInTargetFile.length / 20;
        
        int pieceLength = torrentMetaInfo.getInfo().getPieceLength().getValue();
        
        int fileLength = torrentMetaInfo.getInfo().getLength().getValue();
        int peiceLengthCalcuatedFromTorrentInfo = (int)Math.ceil(((double)fileLength)/pieceLength) ; 
        if(peiceLengthCalcuatedFromTorrentInfo != piecesCount){
        	throw new TorrentException("Calcuated size and pieces does not match");
        }
        
        
        torrentContext.setPiecesCount(piecesCount);
        PieceInfo[] pieceInfoData = new PieceInfo[piecesCount];
        for (int i = 0; i < pieceInfoData.length-1; i++) {
            String hash = new String(piecesInTargetFile, i * 20, 20);
            //boolean isHashSame = Utils.compareByteArrays("hello".getBytes(),"hello".getBytes());
//        	boolean isHashSame = Utils.compareByteArrays(hash.getBytes(), Utils.calucateHash(torrentContext.getSeedDataFile(), pieceLength));
//        	System.out.println("HashTest = " +isHashSame);

            pieceInfoData[i] = new PieceInfo(i, hash, pieceLength);
            piecesToBeDownloaded.add(pieceInfoData[i]);
        }
        
        //We need to specially handle the last piece as it might not fit in to complete piece length, 
        //Unless we do treat it specially, the hash check will fail
        int i = pieceInfoData.length-1;
        String hash = new String(piecesInTargetFile, i * 20, 20);
        //We initalize the piece size to what ever left
        pieceInfoData[i] = new PieceInfo(i, hash, fileLength-i*pieceLength);
        piecesToBeDownloaded.add(pieceInfoData[i]);
        
        
        storageManager = new MemoryBasedStorageManager(torrentContext);
        if (torrentContext.isSeed()) {
            storageManager.initAsSeed(torrentContext.getSeedDataFile(), pieceInfoData,fileLength);
        } else {
            storageManager.initialize(pieceInfoData, new File(torrentContext.getDestinationFolder()),fileLength);
        }
        torrentContext.setStorageManager(storageManager);

        bittorrentServer = new BittorrentServer(torrentContext.getPort(), this);
        bittorrentServer.start();
    }

    private void initInfoHash() throws TorrentException {
//    	 get the info value and compute it's hash

        try {
            InfoDictionary info = torrentMetaInfo.getInfo();
            ByteArrayOutputStream infoBytes = new ByteArrayOutputStream();
            info.serialize(infoBytes);
            infoHash = Utils.computeHash(infoBytes.toByteArray());

            torrentContext.setInfoHash(infoHash);

        } catch (NoSuchAlgorithmException e) {
            throw new TorrentException(e);
        } catch (IOException e) {
            throw new TorrentException(e);
        }
    }

    public void run() {


        try {
            // TODO
            // create piece info list for all pieces and initialize them to be downloaded
            TrackerResponse response = updateFromTracker();

            if (torrentContext.isSeed()) {
                //If I am a seed, I will just keep runing so ther can connect to me. but do not try to do anything else
                while (true) {
                    Thread.sleep(2000);
                }
            } else {
                // let's try to connect to the peers list provided by the tracker to make sure they are
                // still alive ;)

                // PeerDictionarycreate peer instances for peers in tracker response. Which peers to connect?
                List<PeerDictionary> livePeers = Utils.findLivePeers(response.getPeers());
                if (livePeers.size() == 0) {
                    throw new TorrentException("No live peer found, can not continue");
                }

                List<PeerDictionary> peersToConnect = selectPeersToConnect(livePeers);

                // create thread for each instance and start thread
                for (PeerDictionary apeerData : peersToConnect) {
                    //log.info("Connecting to Peer " + apeerData.getPeerId());
                    //TODO make sure passing info hash as a string is correct
                	if(myIpAddress.equals(apeerData.getIP()) && myPort == apeerData.getPort()){
                		//Do not connect to myself
                	}else{
                		System.out.println("Connected to peer " +livePeers);
                        PeerInstance peer = new PeerInstance(infoHash, apeerData.getIP(), apeerData.getPort(), this, torrentContext.getMyPeerId());
                        torrentContext.addPeer(peer, false);
                        new Thread(peer).start();
                	}
                }

                // waiting for handshakes to complete
                Thread.sleep(2 * 1000);

                int count = 5;

                int currentlyBusyPeers = 0;

                peersJustGotBusy = new HashMap<String, Boolean>(); // this contains the list of peers just started downloading. don't laugh for the name ;)
                // wait for all pieces to be completed downloading
                while (piecesToBeDownloaded.size() > 0 || piecesBeingDownloaded.size() > 0) {

                    if (currentlyBusyPeers < Constants.MAX_CONCURRENT_CONNECTIONS) {

                        // 1. select rarest n pieces from the one of the unchoked peers
                        PiecePeerMapping[] peerMappings = selectBestPiecesToDownload(count);

                        //  2. Now select a peer which is not currently used to download a piece
                        for (PiecePeerMapping peerMapping : peerMappings) {
                            for (String peerId : peerMapping.getPeers()) {
                                if (!peersJustGotBusy.containsKey(peerId)) {
                                    PeerInstance peerInstance = torrentContext.getOutgoingPeerInformation().get(peerId);
                                    if (peerInstance == null) {
                                        peerInstance = torrentContext.getIncomingPeerInformation().get(peerId);
                                    }

                                    // when we come to this point peer instance MUST BE NOT NULL
                                    //We remove the piece from toBeDownloaded and add it to being downloaded
                                    PieceInfo pieceInfo = torrentContext.getStorageManager().getPieceInfo(peerMapping.getPieceIndex());
                                    synchronized (this) {
                                        piecesToBeDownloaded.remove(pieceInfo);
                                        piecesBeingDownloaded.add(pieceInfo);
                                    }
                                    //Start the download
                                    int pieceIndex = peerMapping.getPieceIndex();
                                    log.info("Asking to download piece number " + pieceIndex + " from the peer " + peerInstance.getID());

                                    peerInstance.startDownload(pieceIndex);
                                    peersJustGotBusy.put(peerId, Boolean.TRUE);
                                    break;
                                }
                            }
                        }

                    }

                    // calculate current downloading peers
                    currentlyBusyPeers = calculateBusyPeerCount();
                }

                System.out.println("Download Complete");

                storageManager.setDownloadCompleted();

                // how long to seed? for ever for now
                while (true) {
                    Thread.sleep(2 * 1000);
                }
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (TorrentException e) {
            e.printStackTrace();
        }


    }


    private int calculateBusyPeerCount() {

        int currentlyBusyPeers = 0;
        for (PeerInstance peerInstance : torrentContext.getIncomingPeerInformation().values()) {
            if (peerInstance.isBusy()) {
                currentlyBusyPeers++;
            }
        }
        for (PeerInstance peerInstance : torrentContext.getOutgoingPeerInformation().values()) {
            if (peerInstance.isBusy()) {
                currentlyBusyPeers++;
            }
        }
        return currentlyBusyPeers;
    }

    private PiecePeerMapping[] selectBestPiecesToDownload(int count) throws TorrentException {

        int pieceCount = torrentContext.getPiecesCount();

        PiecePeerMapping[] pieceMappingList = new PiecePeerMapping[pieceCount];

        for (int i = 0; i < pieceMappingList.length; i++) {
            pieceMappingList[i] = new PiecePeerMapping(i);
        }

        // get the unchoked lists of peers
        for (PeerDescription peerDescription : torrentContext.getPeerDescriptions()) {
            if (!peerDescription.isChoked()) {

                String peerId = peerDescription.getPeerId();
                PeerInstance incomingPeerInstance = torrentContext.getIncomingPeerInformation().get(peerId);
                PeerInstance outgoingPeerInstance = torrentContext.getOutgoingPeerInformation().get(peerId);

                if (incomingPeerInstance == null && outgoingPeerInstance == null) {
                    throw new TorrentException("Both Incoming peer instance and out going peer instance can not null at the same time for peer " + peerId);
                }

                //TODO we can not iterate over 
                synchronized (peerDescription.getPiecesList()) {
                    for (Integer pieceIndex : peerDescription.getPiecesList()) {

                        // if the piece is not already downloaded and the peer instance is not busy, let's
                        // add it to the mapping
                        if (!piecesFinishedDownloading.containsKey(pieceIndex) && ((incomingPeerInstance != null && !incomingPeerInstance.isBusy())
                                || (outgoingPeerInstance != null && !outgoingPeerInstance.isBusy()))) {
                            pieceMappingList[pieceIndex].addPeer(peerDescription.getPeerId());
                        }
                    }
				}
            }
        }

        // select the rarest pieces from them
        Arrays.sort(pieceMappingList);

        // now this piece mapping list contains, pieces that have no peers as well. We need to remove them
        // since this list is already sorted, let's find the index which has no peers for a piece and
        // create a new list from that point forward

        int index;
        for (index = 0; index < pieceMappingList.length; index++) {
            PiecePeerMapping piecePeerMapping = pieceMappingList[index];
            if (piecePeerMapping.getPeers().size() != 0) {
                break;
            }
        }

        PiecePeerMapping[] pieceMappingListWithoutZeros;
        if (count > (pieceCount - index)) {
            pieceMappingListWithoutZeros = new PiecePeerMapping[pieceCount - index];
            System.arraycopy(pieceMappingList, index, pieceMappingListWithoutZeros, 0, pieceCount - index);
        } else {
            pieceMappingListWithoutZeros = new PiecePeerMapping[count];
            System.arraycopy(pieceMappingList, index, pieceMappingListWithoutZeros, 0, count);
        }

        // return the piece -> best peer mapping

        return pieceMappingListWithoutZeros;
    }

    /**
     * Decide which peers to connect at the initialization
     *
     * @param peers
     * @return
     */

    private List<PeerDictionary> selectPeersToConnect(List<PeerDictionary> peers) {
        List<PeerDictionary> selectedPeers = new ArrayList<PeerDictionary>();
        if (peers.size() > Constants.MAX_CONCURRENT_CONNECTIONS) {
            for (int i = 0; i < Constants.MAX_CONCURRENT_CONNECTIONS; i++) {
                selectedPeers.add(peers.get(i));
            }
            return selectedPeers;
        } else {
            return peers;
        }
    }

    private TrackerResponse updateFromTracker() throws TorrentException {
        try {
            TrackerRequest trackerRequest;
            int downloaded = torrentMetaInfo.getInfo().getLength().getValue();
            if (torrentContext.isSeed()) {
                trackerRequest = new TrackerRequestImpl(downloaded, null, myIpAddress,
                        infoHash, 0, torrentContext.getMyPeerId(), myPort, 0);
            } else {
                trackerRequest = new TrackerRequestImpl(0, TrackerRequest.STARTED, myIpAddress,
                        infoHash, torrentMetaInfo.getInfo().getLength().getValue(), torrentContext.getMyPeerId(), myPort, 0);
            }
            // create and call tracker with 'tracker request' message
            HTTPCommunicationManager HTTPCommunicationManager = new HTTPCommunicationManager();
            String trackerURL = torrentMetaInfo.getAnnounce().getValue() + "?" + trackerRequest.getQueryString();
            log.info("tracker URL = " + trackerURL);
            InputStream in = HTTPCommunicationManager.sendReceive(trackerURL);
            return new TrackerResponseImpl(in);


        } catch (IOException e) {
            throw new TorrentException(e);
        }
    }

//    public PieceInfo getPieceToDownload(byte[] availableBitField) {
//        /* TODO */
//        // remove and return a piece from 'to be downloaded' list. How do you do the selection?
//        // move piece to 'being downloaded' list
//        // sync access?
//        return null;
//    }

    public boolean setPieceDownloaded(PieceInfo pieceInfo, String peerId) throws TorrentException {
        /* TODO */
        // check if SHA1hash(piece) validates against its expected SHA1 hash value
        // if so, return true; else return false
        // move piece from 'being downloaded' to 'finished' list
        // save to disk. Keep in cache?
        // send 'have' message for this piece to all peers
        // sync access?
        pieceInfo.setCompleted(true);
//TODO we can put this code in to a better place
        Set<PeerInstance> peers = new HashSet<PeerInstance>();
        peers.addAll(torrentContext.getIncomingPeerInformation().values());
        peers.addAll(torrentContext.getOutgoingPeerInformation().values());
        for (PeerInstance peer : peers) {
            peer.sendHaveMessage(pieceInfo);
        }
        synchronized (this) {
            piecesBeingDownloaded.remove(pieceInfo);
            piecesFinishedDownloading.put(pieceInfo.getIndex(), pieceInfo);
            peersJustGotBusy.remove(peerId);
        }

        log.severe("Completed downloading the piece " + pieceInfo.getIndex());
        return true;

    }

    public void cancelPieceDownload(PieceInfo piece) {
// cancel a piece that was flagged as being downloaded.
// Move piece from 'being downloaded' list to 'to be downloaded' list
        synchronized (this) {
            piecesBeingDownloaded.remove(piece);
            piecesToBeDownloaded.add(piece);
        }
    }

    /**
     * @param index
     * @return
     */
    public synchronized PieceInfo getPieceInfo(int index) {
        //sync accsess
        return torrentContext.getStorageManager().getPieceInfo(index);
    }

    public boolean interestedMessageReceived(PeerInstance peerInstance) {
        // mark this peer id is interested in getting pieces from me
        //TODO decide whether I should respect the request
        return true;
    }

    public TorrentContext getTorrentContext() {
        return torrentContext;
    }

    public static void main(String[] args) {
        final File testTorrent = new File("resources/freeculture.Team_6.pdf.torrent");
        try {
            Properties p = new Properties();
            FileInputStream in = new FileInputStream("resources/bittorrent.properties");
            p.load(in);
            in.close();
            TorrentMetainfo meta =
                    TorrentMetainfoImpl.deserialize(new BufferedInputStream(new FileInputStream(testTorrent)));
            TorrentContext torrentContext = new TorrentContext(p);

            TorrentManager manager = new TorrentManager(meta, torrentContext);
            manager.run();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
